Skip to content

Fix/wallet fee calculation #1916

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open

Fix/wallet fee calculation #1916

wants to merge 4 commits into from

Conversation

OBorce
Copy link
Contributor

@OBorce OBorce commented Apr 29, 2025

  • The initial bug was caused by the calculation of the TX size with 0 inputs, and the bug got triggered when a TX was constructed with 100+ inputs which caused the compact length to have 1 more byte then before.
    This was fixed by passing the number of inputs to the tx_size_with_outputs.
  • Currently as before the fee of a TX can be up to 16 bytes more per change output because of the unknown amount of change that will be returned, and the calculation always uses Amount::MAX.
  • The fees test has been rewritten to have both a TX with many inputs up to 1k and a sweep TX which uses a separate implementation which doesn't have a change output and always pays correctly the exact fee.
  • In the new test a rounding error was discovered, and fixed which happens in case of individual input fee calculation vs total fee calculation using entire tx size.
  • So now we still calculate the fee per input for the selection algorithm to know which input has how much effective value, but at the end after the inputs have been selected we recalculate the fee and add the difference back as change. (it should always be up to number of inputs in atoms, which is not a lot but still was crushing the test)
  • Also added the missing limit for tx size/weight, which now should prevent creating a TX with size over 1MB retrieved from chain_config.

Closes #1208

@OBorce OBorce force-pushed the fix/wallet-fee-calculation branch from 05abe2e to 6b1fe65 Compare May 1, 2025 09:52
OBorce added 3 commits May 6, 2025 12:29
- account for compact input length encoding
- fix rounding error in case of individual input fee calculation vs
  total fee calculation using entire tx size
- add missing limit for tx size/weight
@OBorce OBorce force-pushed the fix/wallet-fee-calculation branch from 6b1fe65 to 5a4271e Compare May 6, 2025 10:30
Comment on lines 384 to 391
let selection_result = select_coins(
utxos,
(amount_to_be_paid_in_currency_with_fees - preselected_amount).unwrap_or(Amount::ZERO),
PayFee::PayFeeWithThisCurrency,
cost_of_change,
selection_algo,
self.chain_config.max_tx_size_for_mempool(),
)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, why do we need to call select_coins in the case when amount_to_be_paid_in_currency_with_fees == preselected_amount?

Also, select_coins has this code:

if selection_target == Amount::ZERO && pay_fees == PayFee::DoNotPayFeeWithThisCurrency {
    return Ok(SelectionResult::new(selection_target));
}

Why doesn't it ignore zero amount in the PayFeeWithThisCurrency case too?

It looks like we select coins for the change even when we don't need any change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed, and fixed some tests that create a delegationId with 0 fee, where the algo would pick now inputs and the tx will fail. I added some small change outputs along the create delegation output to force selecting an input.

(inputs.amount, inputs.fee, inputs.total_input_size)
});

let (size_of_change, cost_of_change) = output_change_size_and_fees(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a tight coupling between output_change_size_and_fees and check_outputs_and_add_change - the former accepts Option<&Address<Destination>> and assumes PublicKeyHash if it's None. And the latter then calls key_chain.next_unused_address if change_addresses doesn't contain the corresponding currency, and next_unused_address is assumed to always return PublicKeyHash I guess, which is not a great assumption either.

I wonder if we could refactor it in this PR. E.g. modify change_addresses in advance, calling key_chain.next_unused_address if necessary, so that the map already contains all possible currencies.
(If not, then it's better to at least add some comments to output_change_size_and_fees and check_outputs_and_add_change, mentioning this interdependency).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed and also moved creation of the change output right after the selection of the inputs to use the specific destination (preselected or next_unued_address).

Comment on lines 2543 to 2558
// increase fee and total_input_size, this is same for all inputs
match preselected_inputs.entry(Currency::Coin) {
Entry::Vacant(entry) => {
entry.insert(PreselectedInputAmounts {
amount: Amount::ZERO,
fee,
burn: Amount::ZERO,
total_input_size,
});
}
Entry::Occupied(mut entry) => {
let existing = entry.get_mut();
existing.fee = (existing.fee + fee).ok_or(WalletError::OutputAmountOverflow)?;
existing.total_input_size += total_input_size;
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks hacky.

  1. select_inputs_for_send_request operates on pay_fee_with_currency, so I guess you should pass it here instead of assuming that it's always Coin.
    (or if you think it's redundant, then maybe select_inputs_for_send_request should no longer pretend that it can handle other currencies to pay fees with).
  2. The code in select_inputs_for_send_request, where it iterates over "non-pay_fee_with_currency" currencies and updates the total tx size and fee, doesn't make sense anymore, because those values will always be zeroes now.

So,
a) I wonder what was wrong with the previous approach. Why can't we return the sizes/fees on per-currency basis as before, and make the caller combine the result (like it still does)?
b) If the old approach is not good anymore, then maybe this function's return type should be changed to something else that better reflects the data that is being collected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed those fields from the PreselectedInputAmounts, and created a new struct as the size and fees are unrelated to the currency.

@OBorce OBorce force-pushed the fix/wallet-fee-calculation branch 2 times, most recently from 025bb56 to 8184900 Compare May 8, 2025 16:15
@OBorce OBorce force-pushed the fix/wallet-fee-calculation branch from 8184900 to be73675 Compare May 8, 2025 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Limit Wallet transaction size
2 participants